VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
  Persistable = 0  'NotPersistable
  DataBindingBehavior = 0  'vbNone
  DataSourceBehavior  = 0  'vbNone
  MTSTransactionMode  = 0  'NotAnMTSObject
END
Attribute VB_Name = "ISprite_Mar"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'====================================================================================
'ISprite_Mar - Class to store Pac Mar and move her around the level
'====================================================================================
Option Explicit
Implements ISprite

Public PositionX As Long
Public PositionY As Long

Private MyDirection As PM_DIRECTION  'Mar's current direction
Public Pending As PM_DIRECTION       'Next direction to go at a junction
Public IsMoving As Boolean           'If Mar is in motion between tiles
Public Speed As Long                 'Pixels per frame speed

'Which animation to show
Public Enum PM_MAR_ANIM
    MA_Up = 0
    MA_Right = 1
    MA_Down = 2
    MA_Left = 3
    MA_Dead = 4
End Enum
Private CurrentAnim As PM_MAR_ANIM

'For the turbo powerup, previous locations are remembered to create a streaming
'trail as mar moves
Private Type PM_MAR_TURBOFRAME
    Anim As PM_MAR_ANIM
    FrameNumber As Long
    Position As PM_XY
End Type
Private TurboFrames(4) As PM_MAR_TURBOFRAME
Private TurboFrame As Long

'====================================================================================
'CLASS Initialize: When this class is created, prepare the sprite
'====================================================================================
Private Sub Class_Initialize()
    'Asign defaults
    Me.Direction = DIR_LEFT: Pending = 0
    IsMoving = False: Speed = 5
End Sub

'This lot implements the `ISprite` interface to provide compatibility with the _
 ghost sprites who do the same. See 'IScreen' class for details
Private Property Get ISPrite_Direction() As PM_DIRECTION: ISPrite_Direction = MyDirection: End Property
Private Property Let ISPrite_Direction(ByVal NewDirection As PM_DIRECTION): Direction = NewDirection: End Property
Private Property Get ISPrite_Pending() As PM_DIRECTION: ISPrite_Pending = Pending: End Property
Private Property Let ISPrite_Pending(ByVal NewDirection As PM_DIRECTION): Pending = NewDirection: End Property
Private Property Get ISprite_XPosition() As Long: ISprite_XPosition = PositionX: End Property
Private Property Let ISprite_XPosition(ByVal Value As Long): PositionX = Value: End Property
Private Property Get ISprite_YPosition() As Long: ISprite_YPosition = PositionY: End Property
Private Property Let ISprite_YPosition(ByVal Value As Long): PositionY = Value: End Property
Private Property Get ISprite_Speed() As Long: ISprite_Speed = Speed: End Property
Private Property Let ISprite_Speed(ByVal Value As Long): Speed = Value: End Property
Private Property Get ISprite_IsMoving() As Boolean: ISprite_IsMoving = IsMoving: End Property
Private Property Let ISprite_IsMoving(ByVal Value As Boolean): IsMoving = Value: End Property

'LET Direction: When Pac Mar's direction is set, set the right animation to match
'====================================================================================
Public Property Let Direction(ByVal NewDirection As PM_DIRECTION)
    'Assign the direction
    MyDirection = NewDirection
    Select Case MyDirection
        Case DIR_UP: CurrentAnim = MA_Up          'Select Up animation
        Case DIR_RIGHT: CurrentAnim = MA_Right    '...
        Case DIR_DOWN: CurrentAnim = MA_Down
        Case DIR_LEFT: CurrentAnim = MA_Left
    End Select
End Property

'GET Direction: Return Mar's current direction to the caller
'====================================================================================
Public Property Get Direction() As PM_DIRECTION: Direction = MyDirection: End Property

'====================================================================================
'ISPRITE Draw: Draw Mar on the level, in her current direction and frame of animation
'====================================================================================
Public Sub Draw(ByVal hDC As Long): ISprite_Draw hDC: End Sub
Private Sub ISprite_Draw(ByVal hDC As Long)
    'If the turbo effect is active
    If TurboFrame > 0 Then
        Dim K As Long
        'Loop each of the saved previous locations of Mar
        For K = 0 To UBound(TurboFrames): With TurboFrames(K)
            'As long as the frame is valid, paint a transparent Mar in place
            If .Anim >= 0 Then Sprites.Mar(.Anim).DrawEx hDC, (.Position.X - TileSize), (.Position.Y - Level.YScroll), Sprites.Mar(.Anim).Width, Sprites.Mar(.Anim).Height, Sprites.Mar(.Anim).Width * .FrameNumber, 0, 150 - ((150 \ UBound(TurboFrames) + 1) * K)
        End With: Next
    End If
    
    'Paint Mar onto the screen as normal
    Sprites.Mar(CurrentAnim).Draw hDC, (PositionX - TileSize), (PositionY - Level.YScroll)
End Sub

'====================================================================================
'ISPRITE Animate: Animate Mar and move her around the level
'====================================================================================
Public Sub Animate(): ISprite_Animate: End Sub
Private Sub ISprite_Animate()
    'Only move if gameplay has started and the game is not paused
    If Level.Paused Then Exit Sub
    
    '--- Playing --------------------------------------------------------------------
    If ScreenMode = SM_Play Then
        'Whilst the screen itself refreshes 33 times a second, only animate the _
         frames every third frame so that the sprites require less frames; _
         That is unless the turbo powerup is in effect...
        If Level.PickupType = PT_GOOD_TURBO Then
            'Animate once a frame, for quick-chomping
            Sprites.Mar(CurrentAnim).Animate
            
        'When you hold Shift, Mar will slow down to that of the fastest ghost (Dib) _
        'In this case, also slow down the animation
        Else
            If Frame.Count Mod IIf( _
                Me.Speed = Project.DiffModes(Level.Data.Difficulty).DibSpeed, _
                5, 3 _
            ) = 0 Then Sprites.Mar(CurrentAnim).Animate
            
        End If
        
        'Location of Mar in the level
        Dim MarPos As PM_XY
        MarPos.X = GetTileFromPx(Me.PositionX)
        MarPos.Y = GetTileFromPx(Me.PositionY)
        'Number of pixels offset in the tile
        Dim Offset As PM_XY
        Offset.X = Me.PositionX Mod TileSize
        Offset.Y = Me.PositionY Mod TileSize
        
        'Move Mar using the generic sprite moving routine
        Level.MoveSprite Me
        
        'The camera is locked on Mar
        Level.YScroll = Range((Me.PositionY - (3 * TileSize)), TileSize, (Level.Data.Height - 7) * TileSize)
        
        'As Mar moves, check to see if she eats any cookies
        Level.EatCookies Me
        
        'Have you eaten a love heart Power Pill? ----------------------------------------
        If Offset.Y <= 10 And MyDirection = DIR_UP Then
            Level.EatHeart MarPos.X, MarPos.Y
        ElseIf Offset.X <= 4 And MyDirection = DIR_LEFT Then
            Level.EatHeart MarPos.X, MarPos.Y
        ElseIf Offset.Y >= 60 And MyDirection = DIR_DOWN Then
            Level.EatHeart MarPos.X, MarPos.Y + 1
        ElseIf Offset.X >= 54 And MyDirection = DIR_RIGHT Then
            Level.EatHeart MarPos.X + 1, MarPos.Y
        End If
        
        'Is the pickup on the screen? ---------------------------------------------------
        If Level.PickupWaiting = True Then
            'Is Pac Mar within eating distance?
            If (MarPos.X = Level.Data.PenX) And (MarPos.Y = Level.Data.PenY - 1) Then
                'Check if Mar has actually eaten the pie
                If (MarPos.X = Level.Data.PenX) And (Offset.X >= 14 And Offset.X <= 50) And _
                   (Offset.Y = 0) _
                Then
                    'The pie has been eaten!
                    Level.EatPie
                    'If 2x Multiplier, add 2x Particle
                    If Level.PickupType = PT_GOOD_MULTIPLIER Then
                        GameMedia.Particles.Create PT_X2, Me.PositionX + HalfTileSize, Me.PositionY + HalfTileSize, 25
                    End If
                End If
            End If
        End If
        
        'Note: Pending direction means that you can press a direction before Mar _
               has reached a junction and she will turn. Without pending direction _
               in use, Mar would only turn if a key is held down at the point she _
               meets the junction (very difficult to judge when it comes down to _
               the pixel)
        If Me.IsMoving = True Then
            'If Mar is currently half way between two tiles, allow her to about-face
            p_ReverseDirection
            'Allow Mar to queue up the direction to turn at the next junction
            p_PendingDirection
            'Make the running noise as she goes
            SFX.Running.Play
            
        Else
            'If Mar is waiting at a corner, allow her to choose a new direction
            p_ChangeDirection
        End If
    
    
    '--- Dead / Game Over -----------------------------------------------------------
    ElseIf ScreenMode = SM_Dead Or ScreenMode = SM_GameOver Then
        Static Wait As Long
        
        'When Mar dies, first wait for a second, and then animate
        If ModeStep = 0 Then
            Wait = Wait + 1
            If Wait = 32 Then Wait = 0: ModeStep = 1
            
        ElseIf ModeStep = 1 Then
            'Animate her dying (ain't that mean!)
            If (Frame.Count Mod 3) = 0 Then Sprites.Mar(MA_Dead).Animate
            If Sprites.Mar(MA_Dead).CurrentFrame = (Sprites.Mar(MA_Dead).FrameCount - 1) Then ModeStep = 2
            
        End If
    End If
    
    'When Mar gets the turbo powerup, a stream of onion-skin style copies of Mar _
     stream behind her. This is done by recording snapshots of Mar's location over _
     time, and updating the stream as she moves. A snapshot is taken every 2 frames:
    If (Frame.Count Mod 2) = 0 Then
        '`TurboFrame` defines how many of these snapshots to show. This expands up _
         to the maximum when the turbo effect is enabled and then reduces to 0 once _
         the turbo runs out. This also lets the previous snapshots fade out when _
         Mar stops against a wall
        If TurboFrame > 0 Then
            'Shuffle the snapshots down, leaving an empty slot at the bottom
            Dim K As Long
            For K = UBound(TurboFrames) To 1 Step -1
                TurboFrames(K).Anim = TurboFrames(K - 1).Anim
                TurboFrames(K).FrameNumber = TurboFrames(K - 1).FrameNumber
                TurboFrames(K).Position.X = TurboFrames(K - 1).Position.X
                TurboFrames(K).Position.Y = TurboFrames(K - 1).Position.Y
            Next
        End If
        
        'If the turbo power is in effect,
        If Level.PickupType = PT_GOOD_TURBO And IsMoving = True Then
            'Take a new snapshot of Mar's position
            With TurboFrames(0)
                .Anim = CurrentAnim
                .FrameNumber = Sprites.Mar(CurrentAnim).CurrentFrame
                .Position.X = Me.PositionX
                .Position.Y = Me.PositionY
            End With
            'Increase the number of snapshots, up to the maximum
            TurboFrame = Range(TurboFrame + 1, 0, UBound(TurboFrames))
        Else
            'When the turbo runs out, the number of snapshots is reduced until 0, _
             and thus the stream behind Mar fades out
            TurboFrames(0).Anim = -1
            TurboFrame = Range(TurboFrame - 1, 0, UBound(TurboFrames))
            
        End If
    End If
End Sub

'====================================================================================
'ISPRITE MakeDecision: Determine where to turn at a junction
'====================================================================================
Public Function MakeDecision(ByVal TileX As Long, ByVal TileY As Long) As PM_DIRECTION: MakeDecision = ISprite_MakeDecision(TileX, TileY): End Function
Private Function ISprite_MakeDecision(ByVal TileX As Long, ByVal TileY As Long) As PM_DIRECTION
    'Specify the queued direction as the direction to go at the next junction
    ISprite_MakeDecision = Me.Pending
End Function

'====================================================================================
'PRIVATE ChangeDirection: When Mar is at a junction, allow her to change direction
'====================================================================================
Private Function p_ChangeDirection()
    Dim NewDirection As PM_DIRECTION
    
    Dim KeyRight As VK, KeyLeft As VK, KeyUp As VK, KeyDown As VK
    If Level.PickupType = PT_BAD_REVERSE Then
        KeyRight = VK_LEFT:   KeyLeft = VK_RIGHT
        KeyUp = VK_DOWN: KeyDown = VK_UP
    Else
        KeyRight = VK_RIGHT: KeyLeft = VK_LEFT
        KeyUp = VK_UP: KeyDown = VK_DOWN
    End If
    
    'Get the direction the user wants, ignore key presses for the direction you're _
     already going. Google for GetKeyState to see how it works. Basically it tells _
     you if a key is pressed or not (regardless if others are pressed as well)
    Select Case True
        Case (MyDirection <> DIR_LEFT) And (SharedAPI.GetKeyState(KeyLeft) And &H8000): NewDirection = DIR_LEFT
        Case (MyDirection <> DIR_RIGHT) And (SharedAPI.GetKeyState(KeyRight) And &H8000): NewDirection = DIR_RIGHT
        Case (MyDirection <> DIR_DOWN) And (SharedAPI.GetKeyState(KeyDown) And &H8000): NewDirection = DIR_DOWN
        Case (MyDirection <> DIR_UP) And (SharedAPI.GetKeyState(KeyUp) And &H8000): NewDirection = DIR_UP
    End Select
    
    'You cannot change the direction *into* a wall. (i.e. If the direction value, _
     either 1, 2, 4 or 8 equals the wall value - also 1, 2, 4 or 8)
    If (Level.Data.Tile(GetTileFromPx(Me.PositionX), GetTileFromPx(Me.PositionY)) And NewDirection) <> NewDirection Then
        Me.Direction = NewDirection
    End If
End Function

'====================================================================================
'PRIVATE ReverseDirection: About-face whilst halfway down a tunnel
'====================================================================================
Private Function p_ReverseDirection() As Boolean
    'Allow the user to reverse their direction whilst only half way through a tile.
    'Only reversing direction is allowed, as you have to turn at a junction at the _
     specific spot
    Dim KeyRight As VK, KeyLeft As VK, KeyUp As VK, KeyDown As VK
    If Level.PickupType = PT_BAD_REVERSE Then
        KeyRight = VK_LEFT:   KeyLeft = VK_RIGHT
        KeyUp = VK_DOWN: KeyDown = VK_UP
    Else
        KeyRight = VK_RIGHT: KeyLeft = VK_LEFT
        KeyUp = VK_UP: KeyDown = VK_DOWN
    End If
    
    Select Case True
        'If any change in direction is made, true is returned
        Case (MyDirection = DIR_LEFT) And (SharedAPI.GetKeyState(KeyRight) And &H8000): Me.Direction = DIR_RIGHT: p_ReverseDirection = True
        Case (MyDirection = DIR_RIGHT) And (SharedAPI.GetKeyState(KeyLeft) And &H8000): Me.Direction = DIR_LEFT: p_ReverseDirection = True
        Case (MyDirection = DIR_DOWN) And (SharedAPI.GetKeyState(KeyUp) And &H8000): Me.Direction = DIR_UP: p_ReverseDirection = True
        Case (MyDirection = DIR_UP) And (SharedAPI.GetKeyState(KeyDown) And &H8000): Me.Direction = DIR_DOWN: p_ReverseDirection = True
        Case Else
            'If the user did not reverse direction this time, return false
            p_ReverseDirection = False
    End Select
End Function

'====================================================================================
'PRIVATE PendingDirection: Allow Mar to choose a direction just before the junction
'====================================================================================
Private Function p_PendingDirection() As PM_DIRECTION
    'Mar can only change direction when she is lined up with the junction, if you _
     only press they key to change direction rather than hold it, you may miss the _
     precise point where the junction is available. This function remembers which _
     direction you chose just before a junction
    Dim KeyRight As VK, KeyLeft As VK, KeyUp As VK, KeyDown As VK
    If Level.PickupType = PT_BAD_REVERSE Then
        KeyRight = VK_LEFT:   KeyLeft = VK_RIGHT
        KeyUp = VK_DOWN: KeyDown = VK_UP
    Else
        KeyRight = VK_RIGHT: KeyLeft = VK_LEFT
        KeyUp = VK_UP: KeyDown = VK_DOWN
    End If
    
    Select Case True
        Case (SharedAPI.GetKeyState(KeyLeft) And &H8000): Me.Pending = DIR_LEFT
        Case (SharedAPI.GetKeyState(KeyRight) And &H8000): Me.Pending = DIR_RIGHT
        Case (SharedAPI.GetKeyState(KeyDown) And &H8000): Me.Pending = DIR_DOWN
        Case (SharedAPI.GetKeyState(KeyUp) And &H8000): Me.Pending = DIR_UP
    End Select
    'Ignore key presses for the direction you are alrady going
    Me.Pending = RemoveBits(Me.Pending, MyDirection)
    'Ignore directions you can't turn into anyway
    Me.Pending = RemoveBits(Me.Pending, Level.Data.Tile(GetTileFromPx(Me.PositionX) - (MyDirection = DIR_RIGHT), GetTileFromPx(Me.PositionY) - (MyDirection = DIR_DOWN)) And PM_WALL_ALL)
End Function

'====================================================================================
'Die: When Pac Mar gets hit by a ghost, change animation
'====================================================================================
Public Sub Die()
    'Switch to the death animation
    CurrentAnim = MA_Dead: Sprites.Mar(MA_Dead).CurrentFrame = 0
    'Play death sound
    SFX.Death.Play
End Sub

'=== END ============================================================================
'This work is licenced under the Creative Commons Attribution 2.5 License
